﻿namespace Bowlus.FrontEnd.RequestRouting
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Reactive.Linq;
    using Bowlus.Common;
    using Bowlus.Common.Communications;
    using Bowlus.FrontEnd.RequestRouting.ApplicationDeployment;
    using Bowlus.FrontEnd.RequestRouting.BlobLeaseUtil;
    using Microsoft.WindowsAzure.ServiceRuntime;

    public class RoutingManager
    {
        public const string DefaultLeaseBlobName = "routingmanager";

        private readonly IRoutingConfigManager arrConfigManager;

        private readonly TenantDeploymentsStore tenantTableContext;

        private readonly IApplicationDeploymentStrategy applicationDeploymentStrategy;

        private readonly IObservable<long> leaseRenewTimer;

        private readonly IObservable<long> healthMonitorTimer;

        private readonly IBlobLease blobLease;

        private readonly Dictionary<Guid, ApplicationDeploymentRequest> messageTracker;

        public RoutingManager(
            IRoutingConfigManager arrConfigManager, IApplicationDeploymentStrategy deploymentStrategy, IBlobLease blobLease)
        {
            this.arrConfigManager = arrConfigManager;
            this.tenantTableContext = TenantDeploymentsStore.Instance;
            this.applicationDeploymentStrategy = deploymentStrategy;
            this.blobLease = blobLease;
            this.messageTracker = new Dictionary<Guid, ApplicationDeploymentRequest>();

            this.leaseRenewTimer = Observable.Interval(TimeSpan.FromSeconds(50));
            this.healthMonitorTimer = Observable.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(60));
        }

        public event EventHandler<RequestRoutingManagerEventArgs> StatusChange;

        public void Initialize()
        {
            Trace.Write("Initializing");
            // Common initialize work for first and subsequent instances. Setup the local routing environment.
            this.arrConfigManager.Initialize();
            var applicationHosts = GetRunningAppHostsWithHttpEndpt().ToList();
            this.arrConfigManager.UpdateApplicationHostsConfiguration(applicationHosts);
            var tenantDeployments = this.tenantTableContext.TenantDeployment.ToList();

            this.blobLease.AcquireLease();

            if (this.blobLease.LeaseStatus != BlobLeaseStatus.Acquired && !RoleEnvironment.IsEmulated)
            {
                Trace.TraceInformation("Not the first instance of the app host.");
                // No need to initiate a redeployment, since this is the subsequent instance. Just update the routing rules
                var runningTenantDeployments = (from deployment in tenantDeployments
                                                where deployment.Status.Equals(TenantDeploymentStatus.Running)
                                                select deployment).ToList();

                this.arrConfigManager.UpdateRoutingRules(runningTenantDeployments);

                this.OnStatusChange(Guid.Empty, "Not the first instance, just updated the routing configuration.");
                return;
            }

            Trace.WriteLine("First instance, managed to acquire the lease.");

            // Subscribe to the timer and start it
            this.leaseRenewTimer.Subscribe(s => this.blobLease.RenewLease());
            this.healthMonitorTimer.Subscribe(s => this.HealthMonitor());

            Trace.WriteLine("Waiting for appHost instances to start up...");
            HostingInfrastructureStore.WaitForAppHostToStart();
            Trace.WriteLine("All appHost instances started up.");

            // Change the status of all the instances to initializing
            foreach (var tenantDeployment in tenantDeployments)
            {
                tenantDeployment.Status = TenantDeploymentStatus.Initializing;
            }

            TenantDeploymentsStore.UpdateDeploymentStore(tenantDeployments);

            // We had the Http endpoint's address in the applicationHosts list at first time. 
            // Need to change it to the deploymentManager's endpoint to send the deploy messages to 
            var appHostsWithDeplEndpt = from host in applicationHosts
                                        let instanceOfRole = RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.
                                            FirstOrDefault(ins => ins.Id.Equals(host.InstanceId))
                                        where instanceOfRole != null
                                        select new ApplicationHost
                                                   {
                                                       InstanceId = host.InstanceId,
                                                       Status = host.Status,
                                                       DeploymentManagerEndpoint =
                                                           instanceOfRole.
                                                           InstanceEndpoints[Settings.DeplMgrEndPointName].IPEndpoint
                                                   };

            var tenantChanges = this.applicationDeploymentStrategy.InitializeDeployments(
                appHostsWithDeplEndpt, tenantDeployments);

            this.UpdateTenantDeployments(tenantChanges);
        }

        public void MessageHandler(BowlusMessage message)
        {
            Trace.TraceInformation(string.Format("*** Message {0} received from {1} with details: {2}", Enum.GetName(typeof(BowlusMessageType), message.MessageType), message.Sender, message.Body));

            switch (message.MessageType)
            {
                case BowlusMessageType.DeployApp:
                    this.DeployApplication(message);

                    break;
                case BowlusMessageType.DeleteApp:
                    this.DeleteApplication(message);
                    break;
                case BowlusMessageType.AppHostShuttingDown:
                    Trace.TraceInformation(string.Format("AppHost instance ({0}) signalled to be removed.", message.Sender));
                    var appHostToRemove = new ApplicationHost(message.Body);
                    HandleAppHostsRemoval(appHostToRemove);
                    break;
                case BowlusMessageType.AppHostStarted:
                    // received a 'healhty' message from a known appHost instance.
                    break;
                case BowlusMessageType.AppHostStartingUp:
                    Trace.TraceInformation(string.Format("AppHost instance ({0}) started up.", message.Sender));
                    var appHost = new ApplicationHost(message.Body);
                    HostingInfrastructureStore.AddInstance(Settings.AppHostRoleName, appHost.InstanceId);
                    HostingInfrastructureStore.SignalInstanceStart(Settings.AppHostRoleName, appHost.InstanceId);
                    break;
                case BowlusMessageType.AppDeployed:
                case BowlusMessageType.AppDeleted:
                    if (!this.messageTracker.ContainsKey(message.MessageId))
                    {
                        Trace.TraceError(
                            string.Format(
                                "Request lost with ID {0}, type {1}, body {2} from {3}",
                                message.MessageId,
                                message.MessageType,
                                message.Body,
                                message.Sender));

                        return;
                    }

                    var request = this.messageTracker[message.MessageId];

                    Guid nextMessageId;

                    if (request.CanUpdateConfiguration(message, out nextMessageId))
                    {
                        var newDeployedTenant = new TenantDeployment(message.Body);

                        this.arrConfigManager.UpdateRoutingRules(new List<TenantDeployment> { newDeployedTenant });

                        newDeployedTenant.Status = TenantDeploymentStatus.Running;

                        TenantDeploymentsStore.UpdateDeploymentStore(newDeployedTenant);

                        this.OnStatusChange(message.MessageId, string.Format("Deployment request successful: {0}", message.Body));
                    }

                    // We are done with this message
                    this.messageTracker.Remove(message.MessageId);

                    // But need to check if we will be waiting for another response for the next message for the same deploy request.
                    if (nextMessageId != Guid.Empty)
                    {
                        this.messageTracker.Add(nextMessageId, request);
                    }

                    break;
                case BowlusMessageType.None:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        public void ClearConfiguration()
        {
            this.arrConfigManager.ClearConfiguration();
        }

        private void HealthMonitor()
        {
            var commsClient = new CommunicationsClient();
            foreach (var appHost in RoleEnvironment.Roles[Settings.AppHostRoleName].Instances)
            {
                var response = new BowlusMessage
                {
                    Body = string.Empty,
                    MessageType = BowlusMessageType.Hearthbeat,
                    MessageId = Guid.NewGuid(),
                    Sender = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints[Settings.FrontEndRoleName].IPEndpoint
                };

                commsClient.SendMessageToRoleInstance(
                    response,
                    appHost.InstanceEndpoints[Settings.AppHostRoleName].IPEndpoint,
                    Settings.AppHostRoleName);
            }
        }

        private void HandleAppHostsRemoval(ApplicationHost appHostToRemove)
        {
            var appHosts = GetAppHostsWithApplications().ToList();
            var requests = this.applicationDeploymentStrategy.RemoveHost(appHostToRemove, appHosts).ToList();
            this.UpdateTenantDeployments(requests);
        }

        public void AppHostsChange()
        {
            try
            {
                var appHosts = GetAppHostsWithApplications().ToList();
                var runningAppHosts = appHosts.Select(h => h.DeploymentManagerEndpoint.Address.ToString()).ToList();
                var knownAppHosts = this.arrConfigManager.GetApphostAddresses().ToList();

                var newHosts = runningAppHosts.Except(knownAppHosts).Select(GetApplicationHostFromIp);

                // NOTE: Here we don't have a handle on the removed appHost instance, so this won't work. 
                var removedHosts = knownAppHosts.Except(runningAppHosts).Select(GetApplicationHostFromIp);

                var requests = new List<ApplicationDeploymentRequest>();

                foreach (var newHost in newHosts)
                {
                    requests = requests.Concat(this.applicationDeploymentStrategy.AddHost(newHost, appHosts)).ToList();
                    appHosts.Add(newHost);
                }

                foreach (var removedHost in removedHosts)
                {
                    requests = requests.Concat(this.applicationDeploymentStrategy.RemoveHost(removedHost, appHosts)).ToList();
                    appHosts.Remove(removedHost);
                }

                this.UpdateTenantDeployments(requests);
            }
            catch (Exception exception)
            {
                Trace.TraceError("EXCEPTION: AppHostChange {0}", exception);                
            }
            
        }

        private static ApplicationHost GetApplicationHostFromIp(string ipAddress)
        {
            var instance =
                RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.FirstOrDefault(
                    i => i.InstanceEndpoints.Any(ep => ep.Value.IPEndpoint.Address.ToString().Equals(ipAddress)));

            var appHost = new ApplicationHost();

            if (instance != null)
            {
                appHost.DeploymentManagerEndpoint = instance.InstanceEndpoints[Settings.DeplMgrEndPointName].IPEndpoint;
                appHost.InstanceId = instance.Id;
            }

            return appHost;
        }

        private static IEnumerable<ApplicationHost> GetRunningAppHostsWithHttpEndpt()
        {
            var applicationHostRole = RoleEnvironment.Roles[Settings.AppHostRoleName];

            // We also will need to bring in the instances from the service management API to select the ones that are running
            var serviceManagementWrapper = Settings.GetManagementService();
            var deployment = serviceManagementWrapper.GetDeploymentBySlot(Settings.ServiceName, Settings.DeploymentSlot);

            return from roleInstance in deployment.RoleInstanceList
                   join roleInstanceInEnvironment in applicationHostRole.Instances on roleInstance.InstanceName equals
                       roleInstanceInEnvironment.Id
                   where roleInstance.InstanceStatus.Equals("Ready")
                   select
                       new ApplicationHost
                           {
                               Status = "Ready",
                               InstanceId = roleInstanceInEnvironment.Id,
                               DeploymentManagerEndpoint = roleInstanceInEnvironment.InstanceEndpoints[Settings.AppHostHttpEndpointName].IPEndpoint
                           };
        }

        private static IEnumerable<ApplicationHost> GetAppHostsWithApplications()
        {
            var deployments = TenantDeploymentsStore.GetCurrentDeployments().ToList();
            var appHosts = (from deployment in deployments
                            group deployment by deployment.AppHostInstanceId
                                into deploymentGroup
                                let roleInstance = RoleEnvironment.Roles[Settings.AppHostRoleName].Instances.FirstOrDefault(
               id => id.Id.Equals(deploymentGroup.Key))
                                where roleInstance != null
                                select new ApplicationHost
                                    {
                                        InstanceId = deploymentGroup.Key,
                                        DeploymentManagerEndpoint =
                                            roleInstance.InstanceEndpoints[
                                                    Settings.AppHostRoleName].IPEndpoint,
                                        Deployments = deploymentGroup
                                    });

            var instanceIds = from instance in RoleEnvironment.Roles[Settings.AppHostRoleName].Instances
                              select instance.Id;

            IEnumerable<string> appHostInstanceIds = appHosts.Select(host => host.InstanceId);

            var emptyHosts = instanceIds.Except(appHostInstanceIds);

            return appHosts = appHosts.Concat((from emptyHost in emptyHosts
                                               select new ApplicationHost
                                                   {
                                                       InstanceId = emptyHost,
                                                       DeploymentManagerEndpoint =
                                                           RoleEnvironment
                                                           .Roles[Settings.AppHostRoleName]
                                                           .Instances.First(id => id.Id.Equals(emptyHost))
                                                           .InstanceEndpoints[Settings.AppHostRoleName]
                                                           .IPEndpoint,
                                                       Deployments = new List<TenantDeployment>()
                                                   }).ToList());
        }

        private void DeleteApplication(BowlusMessage message)
        {
            var appHosts = GetAppHostsWithApplications();

            var tenantChangesWithDelete =
                this.applicationDeploymentStrategy.RemoveApplication(
                    new TenantDeployment(message.Body),
                    appHosts);

            this.UpdateTenantDeployments(tenantChangesWithDelete);
        }

        private void DeployApplication(BowlusMessage message)
        {
            var tenantDeployment = new TenantDeployment(message.Body);

            var appHosts = GetAppHostsWithApplications();

            var tenantChanges = this.applicationDeploymentStrategy.AddApplication(tenantDeployment, appHosts);

            this.UpdateTenantDeployments(tenantChanges);
        }

        private void OnStatusChange(Guid requestId, string message)
        {
            var handler = this.StatusChange;
            if (handler != null)
            {
                handler(this, new RequestRoutingManagerEventArgs { RequestId = requestId, StatusMessage = message });
            }
        }

        private void UpdateTenantDeployments(IEnumerable<ApplicationDeploymentRequest> tenantChanges)
        {
            Trace.WriteLine(string.Format("Processing {0} tenantChange messages.", tenantChanges.Count()));
            foreach (var applicationDeploymentRequest in tenantChanges)
            {
                var messageId = applicationDeploymentRequest.SendDeploymentRequest();

                this.messageTracker.Add(messageId, applicationDeploymentRequest);
            }
        }

        #region Sample XML Rewrite URL and ARR Farm/Server

        /*
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
        <system.webserver>
            <rewrite>
                <rules>
                    <clear />
                    <rule name="trent.bowlus.net" patternsyntax="ecmascript" stopprocessing="true">
                        <match url=".*" />
                        <conditions logicalgrouping="matchall" trackallcaptures="false">
                            <add input="{http_host}" pattern="trent.bowlus.net" />
                        </conditions>
                        <action type="rewrite" url="http://trenttest/{r:0}" />
                    </rule>
                    <rule name="webrolewebappfarm" stopprocessing="true">
                        <match url=".*" />
                        <conditions logicalgrouping="matchall" trackallcaptures="false" />
                        <action type="rewrite" url="http://fullscale180/{r:0}" />
                    </rule>
                </rules>
            </rewrite>
        </system.webserver>
    </configuration>

    <webFarms>
        <webFarm name="WebRoleWebAppFarm" enabled="true">
            <server address="10.115.80.73" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
            <server address="10.115.72.84" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
            <server address="10.115.88.49" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
            <applicationRequestRouting>
                <affinity useCookie="true" />
                <loadBalancing algorithm="WeightedRoundRobin" />
            </applicationRequestRouting>
        </webFarm>
        <webFarm name="TrentTest" enabled="true">
            <server address="10.115.88.49" enabled="true">
                <applicationRequestRouting httpPort="8080" />
            </server>
        </webFarm>
        <applicationRequestRouting>
            <hostAffinityProviderList>
                <add name="Microsoft.Web.Arr.HostNameRoundRobin" />
                <add name="Microsoft.Web.Arr.HostNameMemory" />
            </hostAffinityProviderList>
        </applicationRequestRouting>
    </webFarms>
     */

        #endregion
    }
}
